Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[libc++] Replace __compressed_pair with [[no_unique_address]] #76756

Merged
merged 1 commit into from
Sep 16, 2024

Conversation

philnik777
Copy link
Contributor

@philnik777 philnik777 commented Jan 2, 2024

This significantly simplifies the code, improves compile times and improves the object layout of types using __compressed_pair in the unstable ABI. The only downside is that this is extremely ABI sensitive and pedantically breaks the ABI for empty final types, since the address of the subobject may change. The ABI of the whole object should not be affected.

Fixes #91266
Fixes #93069

@philnik777 philnik777 requested a review from a team as a code owner January 2, 2024 21:50
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Jan 2, 2024
@philnik777
Copy link
Contributor Author

CC @cjdb

@philnik777 philnik777 added enhancement Improving things as opposed to bug fixing, e.g. new or missing feature ABI Application Binary Interface labels Jan 2, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Jan 2, 2024

@llvm/pr-subscribers-libcxx

Author: Nikolas Klauser (philnik777)

Changes

This significantly simplifies the code, improves compile times and improves the object layout of types using __compressed_pair in the unstable ABI. The only downside is that this is extremely ABI sensitive and pedantically breaks the ABI for empty final types, since the address of the subobject may change. The ABI of the whole object should not be affected.


Patch is 138.56 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/76756.diff

21 Files Affected:

  • (modified) libcxx/include/__config (+2)
  • (modified) libcxx/include/__functional/function.h (+14-13)
  • (modified) libcxx/include/__hash_table (+87-68)
  • (modified) libcxx/include/__memory/compressed_pair.h (+20-142)
  • (modified) libcxx/include/__memory/shared_ptr.h (+21-24)
  • (modified) libcxx/include/__memory/unique_ptr.h (+69-51)
  • (modified) libcxx/include/__split_buffer (+12-15)
  • (modified) libcxx/include/__tree (+28-25)
  • (modified) libcxx/include/deque (+25-20)
  • (modified) libcxx/include/forward_list (+11-10)
  • (modified) libcxx/include/future (+12-10)
  • (modified) libcxx/include/list (+12-9)
  • (modified) libcxx/include/string (+69-75)
  • (modified) libcxx/include/vector (+50-53)
  • (modified) libcxx/test/libcxx/containers/sequences/deque/abi.compile.pass.cpp (+28)
  • (added) libcxx/test/libcxx/containers/sequences/list/abi.compile.pass.cpp (+86)
  • (added) libcxx/test/libcxx/containers/sequences/vector.bool/abi.compile.pass.cpp (+66)
  • (added) libcxx/test/libcxx/containers/sequences/vector/abi.compile.pass.cpp (+86)
  • (modified) libcxx/test/libcxx/containers/unord/unord.set/missing_hash_specialization.verify.cpp (+1-1)
  • (removed) libcxx/test/libcxx/memory/compressed_pair/compressed_pair.pass.cpp (-52)
  • (modified) libcxx/test/libcxx/utilities/memory/util.smartptr/util.smartptr.shared/libcxx.control_block_layout.pass.cpp (+123)
diff --git a/libcxx/include/__config b/libcxx/include/__config
index adff13e714cb64..e6d167140c99a0 100644
--- a/libcxx/include/__config
+++ b/libcxx/include/__config
@@ -168,6 +168,8 @@
 // pointer from 16 to 8. This changes the output of std::string::max_size,
 // which makes it ABI breaking
 #    define _LIBCPP_ABI_STRING_8_BYTE_ALIGNMENT
+// Don't add padding from __compressed_pair
+#    define _LIBCPP_ABI_NO_COMPRESSED_PAIR_PADDING
 #  elif _LIBCPP_ABI_VERSION == 1
 #    if !(defined(_LIBCPP_OBJECT_FORMAT_COFF) || defined(_LIBCPP_OBJECT_FORMAT_XCOFF))
 // Enable compiling copies of now inline methods into the dylib to support
diff --git a/libcxx/include/__functional/function.h b/libcxx/include/__functional/function.h
index 6505bb5871739d..e8c2b5040a7ddc 100644
--- a/libcxx/include/__functional/function.h
+++ b/libcxx/include/__functional/function.h
@@ -138,45 +138,46 @@ class __default_alloc_func;
 
 template <class _Fp, class _Ap, class _Rp, class... _ArgTypes>
 class __alloc_func<_Fp, _Ap, _Rp(_ArgTypes...)> {
-  __compressed_pair<_Fp, _Ap> __f_;
+  _LIBCPP_COMPRESSED_PAIR(_Fp, __func_, _Ap, __alloc_);
 
 public:
   typedef _LIBCPP_NODEBUG _Fp _Target;
   typedef _LIBCPP_NODEBUG _Ap _Alloc;
 
-  _LIBCPP_HIDE_FROM_ABI const _Target& __target() const { return __f_.first(); }
+  _LIBCPP_HIDE_FROM_ABI const _Target& __target() const { return __func_; }
 
   // WIN32 APIs may define __allocator, so use __get_allocator instead.
-  _LIBCPP_HIDE_FROM_ABI const _Alloc& __get_allocator() const { return __f_.second(); }
+  _LIBCPP_HIDE_FROM_ABI const _Alloc& __get_allocator() const { return __alloc_; }
 
-  _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(_Target&& __f)
-      : __f_(piecewise_construct, std::forward_as_tuple(std::move(__f)), std::forward_as_tuple()) {}
+  _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(_Target&& __f) : __func_(std::move(__f)), __alloc_() {}
 
-  _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(const _Target& __f, const _Alloc& __a)
-      : __f_(piecewise_construct, std::forward_as_tuple(__f), std::forward_as_tuple(__a)) {}
+  _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(const _Target& __f, const _Alloc& __a) : __func_(__f), __alloc_(__a) {}
 
   _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(const _Target& __f, _Alloc&& __a)
-      : __f_(piecewise_construct, std::forward_as_tuple(__f), std::forward_as_tuple(std::move(__a))) {}
+      : __func_(__f), __alloc_(std::move(__a)) {}
 
   _LIBCPP_HIDE_FROM_ABI explicit __alloc_func(_Target&& __f, _Alloc&& __a)
-      : __f_(piecewise_construct, std::forward_as_tuple(std::move(__f)), std::forward_as_tuple(std::move(__a))) {}
+      : __func_(std::move(__f)), __alloc_(std::move(__a)) {}
 
   _LIBCPP_HIDE_FROM_ABI _Rp operator()(_ArgTypes&&... __arg) {
     typedef __invoke_void_return_wrapper<_Rp> _Invoker;
-    return _Invoker::__call(__f_.first(), std::forward<_ArgTypes>(__arg)...);
+    return _Invoker::__call(__func_, std::forward<_ArgTypes>(__arg)...);
   }
 
   _LIBCPP_HIDE_FROM_ABI __alloc_func* __clone() const {
     typedef allocator_traits<_Alloc> __alloc_traits;
     typedef __rebind_alloc<__alloc_traits, __alloc_func> _AA;
-    _AA __a(__f_.second());
+    _AA __a(__alloc_);
     typedef __allocator_destructor<_AA> _Dp;
     unique_ptr<__alloc_func, _Dp> __hold(__a.allocate(1), _Dp(__a, 1));
-    ::new ((void*)__hold.get()) __alloc_func(__f_.first(), _Alloc(__a));
+    ::new ((void*)__hold.get()) __alloc_func(__func_, _Alloc(__a));
     return __hold.release();
   }
 
-  _LIBCPP_HIDE_FROM_ABI void destroy() _NOEXCEPT { __f_.~__compressed_pair<_Target, _Alloc>(); }
+  _LIBCPP_HIDE_FROM_ABI void destroy() _NOEXCEPT {
+    __func_.~_Fp();
+    __alloc_.~_Alloc();
+  }
 
   _LIBCPP_HIDE_FROM_ABI static void __destroy_and_delete(__alloc_func* __f) {
     typedef allocator_traits<_Alloc> __alloc_traits;
diff --git a/libcxx/include/__hash_table b/libcxx/include/__hash_table
index 3cee48ef8538c6..87fd2984aabb9e 100644
--- a/libcxx/include/__hash_table
+++ b/libcxx/include/__hash_table
@@ -525,29 +525,29 @@ class __bucket_list_deallocator {
   typedef allocator_traits<allocator_type> __alloc_traits;
   typedef typename __alloc_traits::size_type size_type;
 
-  __compressed_pair<size_type, allocator_type> __data_;
+  _LIBCPP_COMPRESSED_PAIR(size_type, __data_, allocator_type, __alloc_);
 
 public:
   typedef typename __alloc_traits::pointer pointer;
 
   _LIBCPP_HIDE_FROM_ABI __bucket_list_deallocator() _NOEXCEPT_(is_nothrow_default_constructible<allocator_type>::value)
-      : __data_(0, __default_init_tag()) {}
+      : __data_(0) {}
 
   _LIBCPP_HIDE_FROM_ABI __bucket_list_deallocator(const allocator_type& __a, size_type __size)
       _NOEXCEPT_(is_nothrow_copy_constructible<allocator_type>::value)
-      : __data_(__size, __a) {}
+      : __data_(__size), __alloc_(__a) {}
 
   _LIBCPP_HIDE_FROM_ABI __bucket_list_deallocator(__bucket_list_deallocator&& __x)
       _NOEXCEPT_(is_nothrow_move_constructible<allocator_type>::value)
-      : __data_(std::move(__x.__data_)) {
+      : __data_(std::move(__x.__data_)), __alloc_(std::move(__x.__alloc_)) {
     __x.size() = 0;
   }
 
-  _LIBCPP_HIDE_FROM_ABI size_type& size() _NOEXCEPT { return __data_.first(); }
-  _LIBCPP_HIDE_FROM_ABI size_type size() const _NOEXCEPT { return __data_.first(); }
+  _LIBCPP_HIDE_FROM_ABI size_type& size() _NOEXCEPT { return __data_; }
+  _LIBCPP_HIDE_FROM_ABI size_type size() const _NOEXCEPT { return __data_; }
 
-  _LIBCPP_HIDE_FROM_ABI allocator_type& __alloc() _NOEXCEPT { return __data_.second(); }
-  _LIBCPP_HIDE_FROM_ABI const allocator_type& __alloc() const _NOEXCEPT { return __data_.second(); }
+  _LIBCPP_HIDE_FROM_ABI allocator_type& __alloc() _NOEXCEPT { return __alloc_; }
+  _LIBCPP_HIDE_FROM_ABI const allocator_type& __alloc() const _NOEXCEPT { return __alloc_; }
 
   _LIBCPP_HIDE_FROM_ABI void operator()(pointer __p) _NOEXCEPT { __alloc_traits::deallocate(__alloc(), __p, size()); }
 };
@@ -687,27 +687,27 @@ private:
 
   // --- Member data begin ---
   __bucket_list __bucket_list_;
-  __compressed_pair<__first_node, __node_allocator> __p1_;
-  __compressed_pair<size_type, hasher> __p2_;
-  __compressed_pair<float, key_equal> __p3_;
+  _LIBCPP_COMPRESSED_PAIR(__first_node, __first_node_, __node_allocator, __node_alloc_);
+  _LIBCPP_COMPRESSED_PAIR(size_type, __size_, hasher, __hasher_);
+  _LIBCPP_COMPRESSED_PAIR(float, __max_load_factor_, key_equal, __key_eq_);
   // --- Member data end ---
 
-  _LIBCPP_HIDE_FROM_ABI size_type& size() _NOEXCEPT { return __p2_.first(); }
+  _LIBCPP_HIDE_FROM_ABI size_type& size() _NOEXCEPT { return __size_; }
 
 public:
-  _LIBCPP_HIDE_FROM_ABI size_type size() const _NOEXCEPT { return __p2_.first(); }
+  _LIBCPP_HIDE_FROM_ABI size_type size() const _NOEXCEPT { return __size_; }
 
-  _LIBCPP_HIDE_FROM_ABI hasher& hash_function() _NOEXCEPT { return __p2_.second(); }
-  _LIBCPP_HIDE_FROM_ABI const hasher& hash_function() const _NOEXCEPT { return __p2_.second(); }
+  _LIBCPP_HIDE_FROM_ABI hasher& hash_function() _NOEXCEPT { return __hasher_; }
+  _LIBCPP_HIDE_FROM_ABI const hasher& hash_function() const _NOEXCEPT { return __hasher_; }
 
-  _LIBCPP_HIDE_FROM_ABI float& max_load_factor() _NOEXCEPT { return __p3_.first(); }
-  _LIBCPP_HIDE_FROM_ABI float max_load_factor() const _NOEXCEPT { return __p3_.first(); }
+  _LIBCPP_HIDE_FROM_ABI float& max_load_factor() _NOEXCEPT { return __max_load_factor_; }
+  _LIBCPP_HIDE_FROM_ABI float max_load_factor() const _NOEXCEPT { return __max_load_factor_; }
 
-  _LIBCPP_HIDE_FROM_ABI key_equal& key_eq() _NOEXCEPT { return __p3_.second(); }
-  _LIBCPP_HIDE_FROM_ABI const key_equal& key_eq() const _NOEXCEPT { return __p3_.second(); }
+  _LIBCPP_HIDE_FROM_ABI key_equal& key_eq() _NOEXCEPT { return __key_eq_; }
+  _LIBCPP_HIDE_FROM_ABI const key_equal& key_eq() const _NOEXCEPT { return __key_eq_; }
 
-  _LIBCPP_HIDE_FROM_ABI __node_allocator& __node_alloc() _NOEXCEPT { return __p1_.second(); }
-  _LIBCPP_HIDE_FROM_ABI const __node_allocator& __node_alloc() const _NOEXCEPT { return __p1_.second(); }
+  _LIBCPP_HIDE_FROM_ABI __node_allocator& __node_alloc() _NOEXCEPT { return __node_alloc_; }
+  _LIBCPP_HIDE_FROM_ABI const __node_allocator& __node_alloc() const _NOEXCEPT { return __node_alloc_; }
 
 public:
   typedef __hash_iterator<__node_pointer> iterator;
@@ -991,26 +991,34 @@ inline __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table() _NOEXCEPT_(
     is_nothrow_default_constructible<__bucket_list>::value&& is_nothrow_default_constructible<__first_node>::value&&
         is_nothrow_default_constructible<__node_allocator>::value&& is_nothrow_default_constructible<hasher>::value&&
             is_nothrow_default_constructible<key_equal>::value)
-    : __p2_(0, __default_init_tag()), __p3_(1.0f, __default_init_tag()) {}
+    : __size_(0), __max_load_factor_(1.0f) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 inline __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(const hasher& __hf, const key_equal& __eql)
-    : __bucket_list_(nullptr, __bucket_list_deleter()), __p1_(), __p2_(0, __hf), __p3_(1.0f, __eql) {}
+    : __bucket_list_(nullptr, __bucket_list_deleter()),
+      __first_node_(),
+      __node_alloc_(),
+      __size_(0),
+      __hasher_(__hf),
+      __max_load_factor_(1.0f),
+      __key_eq_(__eql) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(
     const hasher& __hf, const key_equal& __eql, const allocator_type& __a)
     : __bucket_list_(nullptr, __bucket_list_deleter(__pointer_allocator(__a), 0)),
-      __p1_(__default_init_tag(), __node_allocator(__a)),
-      __p2_(0, __hf),
-      __p3_(1.0f, __eql) {}
+      __node_alloc_(__node_allocator(__a)),
+      __size_(0),
+      __hasher_(__hf),
+      __max_load_factor_(1.0f),
+      __key_eq_(__eql) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(const allocator_type& __a)
     : __bucket_list_(nullptr, __bucket_list_deleter(__pointer_allocator(__a), 0)),
-      __p1_(__default_init_tag(), __node_allocator(__a)),
-      __p2_(0, __default_init_tag()),
-      __p3_(1.0f, __default_init_tag()) {}
+      __node_alloc_(__node_allocator(__a)),
+      __size_(0),
+      __max_load_factor_(1.0f) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(const __hash_table& __u)
@@ -1018,17 +1026,20 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(const __hash_table& __u)
                      __bucket_list_deleter(allocator_traits<__pointer_allocator>::select_on_container_copy_construction(
                                                __u.__bucket_list_.get_deleter().__alloc()),
                                            0)),
-      __p1_(__default_init_tag(),
-            allocator_traits<__node_allocator>::select_on_container_copy_construction(__u.__node_alloc())),
-      __p2_(0, __u.hash_function()),
-      __p3_(__u.__p3_) {}
+      __node_alloc_(allocator_traits<__node_allocator>::select_on_container_copy_construction(__u.__node_alloc())),
+      __size_(0),
+      __hasher_(__u.hash_function()),
+      __max_load_factor_(__u.__max_load_factor_),
+      __key_eq_(__u.__key_eq_) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(const __hash_table& __u, const allocator_type& __a)
     : __bucket_list_(nullptr, __bucket_list_deleter(__pointer_allocator(__a), 0)),
-      __p1_(__default_init_tag(), __node_allocator(__a)),
-      __p2_(0, __u.hash_function()),
-      __p3_(__u.__p3_) {}
+      __node_alloc_(__node_allocator(__a)),
+      __size_(0),
+      __hasher_(__u.hash_function()),
+      __max_load_factor_(__u.__max_load_factor_),
+      __key_eq_(__u.__key_eq_) {}
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(__hash_table&& __u) _NOEXCEPT_(
@@ -1036,12 +1047,15 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(__hash_table&& __u) _NOEX
         is_nothrow_move_constructible<__node_allocator>::value&& is_nothrow_move_constructible<hasher>::value&&
             is_nothrow_move_constructible<key_equal>::value)
     : __bucket_list_(std::move(__u.__bucket_list_)),
-      __p1_(std::move(__u.__p1_)),
-      __p2_(std::move(__u.__p2_)),
-      __p3_(std::move(__u.__p3_)) {
+      __first_node_(std::move(__u.__first_node_)),
+      __node_alloc_(std::move(__u.__node_alloc_)),
+      __size_(std::move(__u.__size_)),
+      __hasher_(std::move(__u.__hasher_)),
+      __max_load_factor_(__u.__max_load_factor_),
+      __key_eq_(std::move(__u.__key_eq_)) {
   if (size() > 0) {
-    __bucket_list_[std::__constrain_hash(__p1_.first().__next_->__hash(), bucket_count())] = __p1_.first().__ptr();
-    __u.__p1_.first().__next_                                                              = nullptr;
+    __bucket_list_[std::__constrain_hash(__first_node_.__next_->__hash(), bucket_count())] = __first_node_.__ptr();
+    __u.__first_node_.__next_                                                              = nullptr;
     __u.size()                                                                             = 0;
   }
 }
@@ -1049,17 +1063,19 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(__hash_table&& __u) _NOEX
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::__hash_table(__hash_table&& __u, const allocator_type& __a)
     : __bucket_list_(nullptr, __bucket_list_deleter(__pointer_allocator(__a), 0)),
-      __p1_(__default_init_tag(), __node_allocator(__a)),
-      __p2_(0, std::move(__u.hash_function())),
-      __p3_(std::move(__u.__p3_)) {
+      __node_alloc_(__node_allocator(__a)),
+      __size_(0),
+      __hasher_(std::move(__u.__hasher_)),
+      __max_load_factor_(__u.__max_load_factor_),
+      __key_eq_(std::move(__u.__key_eq_)) {
   if (__a == allocator_type(__u.__node_alloc())) {
     __bucket_list_.reset(__u.__bucket_list_.release());
     __bucket_list_.get_deleter().size()     = __u.__bucket_list_.get_deleter().size();
     __u.__bucket_list_.get_deleter().size() = 0;
     if (__u.size() > 0) {
-      __p1_.first().__next_     = __u.__p1_.first().__next_;
-      __u.__p1_.first().__next_ = nullptr;
-      __bucket_list_[std::__constrain_hash(__p1_.first().__next_->__hash(), bucket_count())] = __p1_.first().__ptr();
+      __first_node_.__next_     = __u.__first_node_.__next_;
+      __u.__first_node_.__next_ = nullptr;
+      __bucket_list_[std::__constrain_hash(__first_node_.__next_->__hash(), bucket_count())] = __first_node_.__ptr();
       size()                                                                                 = __u.size();
       __u.size()                                                                             = 0;
     }
@@ -1073,7 +1089,7 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::~__hash_table() {
   static_assert((is_copy_constructible<hasher>::value), "Hasher must be copy-constructible.");
 #endif
 
-  __deallocate_node(__p1_.first().__next_);
+  __deallocate_node(__first_node_.__next_);
 }
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
@@ -1119,8 +1135,8 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__detach() _NOEXCEPT {
   for (size_type __i = 0; __i < __bc; ++__i)
     __bucket_list_[__i] = nullptr;
   size()                 = 0;
-  __next_pointer __cache = __p1_.first().__next_;
-  __p1_.first().__next_  = nullptr;
+  __next_pointer __cache = __first_node_.__next_;
+  __first_node_.__next_  = nullptr;
   return __cache;
 }
 
@@ -1137,10 +1153,10 @@ void __hash_table<_Tp, _Hash, _Equal, _Alloc>::__move_assign(__hash_table& __u,
   hash_function()       = std::move(__u.hash_function());
   max_load_factor()     = __u.max_load_factor();
   key_eq()              = std::move(__u.key_eq());
-  __p1_.first().__next_ = __u.__p1_.first().__next_;
+  __first_node_.__next_ = __u.__first_node_.__next_;
   if (size() > 0) {
-    __bucket_list_[std::__constrain_hash(__p1_.first().__next_->__hash(), bucket_count())] = __p1_.first().__ptr();
-    __u.__p1_.first().__next_                                                              = nullptr;
+    __bucket_list_[std::__constrain_hash(__first_node_.__next_->__hash(), bucket_count())] = __first_node_.__ptr();
+    __u.__first_node_.__next_                                                              = nullptr;
     __u.size()                                                                             = 0;
   }
 }
@@ -1257,7 +1273,7 @@ void __hash_table<_Tp, _Hash, _Equal, _Alloc>::__assign_multi(_InputIterator __f
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 inline typename __hash_table<_Tp, _Hash, _Equal, _Alloc>::iterator
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::begin() _NOEXCEPT {
-  return iterator(__p1_.first().__next_);
+  return iterator(__first_node_.__next_);
 }
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
@@ -1269,7 +1285,7 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::end() _NOEXCEPT {
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 inline typename __hash_table<_Tp, _Hash, _Equal, _Alloc>::const_iterator
 __hash_table<_Tp, _Hash, _Equal, _Alloc>::begin() const _NOEXCEPT {
-  return const_iterator(__p1_.first().__next_);
+  return const_iterator(__first_node_.__next_);
 }
 
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
@@ -1281,8 +1297,8 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::end() const _NOEXCEPT {
 template <class _Tp, class _Hash, class _Equal, class _Alloc>
 void __hash_table<_Tp, _Hash, _Equal, _Alloc>::clear() _NOEXCEPT {
   if (size() > 0) {
-    __deallocate_node(__p1_.first().__next_);
-    __p1_.first().__next_ = nullptr;
+    __deallocate_node(__first_node_.__next_);
+    __first_node_.__next_ = nullptr;
     size_type __bc        = bucket_count();
     for (size_type __i = 0; __i < __bc; ++__i)
       __bucket_list_[__i] = nullptr;
@@ -1334,7 +1350,7 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__node_insert_unique_perform(__node_po
   // insert_after __bucket_list_[__chash], or __first_node if bucket is null
   __next_pointer __pn = __bucket_list_[__chash];
   if (__pn == nullptr) {
-    __pn          = __p1_.first().__ptr();
+    __pn          = __first_node_.__ptr();
     __nd->__next_ = __pn->__next_;
     __pn->__next_ = __nd->__ptr();
     // fix up __bucket_list_
@@ -1414,7 +1430,7 @@ void __hash_table<_Tp, _Hash, _Equal, _Alloc>::__node_insert_multi_perform(
   size_type __bc = bucket_count();
   size_t __chash = std::__constrain_hash(__cp->__hash_, __bc);
   if (__pn == nullptr) {
-    __pn          = __p1_.first().__ptr();
+    __pn          = __first_node_.__ptr();
     __cp->__next_ = __pn->__next_;
     __pn->__next_ = __cp->__ptr();
     // fix up __bucket_list_
@@ -1499,7 +1515,7 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::__emplace_unique_key_args(_Key const&
     // insert_after __bucket_list_[__chash], or __first_node if bucket is null
     __next_pointer __pn = __bucket_list_[__chash];
     if (__pn == nullptr) {
-      __pn          = __p1_.first().__ptr();
+      __pn          = __first_node_.__ptr();
       __h->__next_  = __pn->__next_;
       __pn->__next_ = __h.get()->__ptr();
       // fix up __bucket_list_
@@ -1677,7 +1693,7 @@ void __hash_table<_Tp, _Hash, _Equal, _Alloc>::__do_rehash(size_type __nbc) {
   if (__nbc > 0) {
     for (size_type __i = 0; __i < __nbc; ++__i)
       __bucket_list_[__i] = nullptr;
-    __next_pointer __pp = __p1_.first().__ptr();
+    __next_pointer __pp = __first_node_.__ptr();
     __next_pointer __cp = __pp->__next_;
     if (__cp != nullptr) {
       size_type __chash       = std::__constrain_hash(__cp->__hash(), __nbc);
@@ -1854,7 +1870,7 @@ __hash_table<_Tp, _Hash, _Equal, _Alloc>::remove(const_iterator __p) _NOEXCEPT {
   // Fix up __bucket_list_
   // if __pn is not in same bucket (before begin is not in same bucket) &&
   //    if __cn->__next_ is not in same bucket (nullptr is not in same bucket)
-  if (__pn == __p1_.first().__ptr() || std::__constrain_hash(__pn->__hash(), __bc) != __chash) {
+  if (__pn == __first_node_.__ptr() || std::__constrain_hash(__pn->__hash(), __bc) != __chash) {
     if (__cn->__next_ == nullptr || std::__constrain_hash(__cn->__next_->__hash(), __bc) !=...
[truncated]

Copy link

github-actions bot commented Jan 2, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

Copy link
Contributor

@cjdb cjdb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for meticulously tracking all this stuff down! It's a welcome change that's long overdue.

My only concern is the one that @huixie90 raised. I wonder if we can get away with not keeping the padding, since it's a very niche situation. Once it catches up to trunk, I'll try building Chromium OS without any padding and see what breaks (that could be a few weeks though, so we should consider other ways to address this).

# define _LIBCPP_COMPRESSED_PAIR(T1, Name1, T2, Name2) \
[[__gnu__::__aligned__(_LIBCPP_ALIGNOF(T2))]] _LIBCPP_NO_UNIQUE_ADDRESS T1 Name1; \
_LIBCPP_COMPRESSED_PAIR_PADDING(T1, Name1##_padding_); \
_LIBCPP_NO_UNIQUE_ADDRESS T2 Name2; \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not looked in detail at the patch. Does this mean the _LIBCPP_NO_UNIQUE_ADDRESS affects the ABI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm missing something, but yes, [[no_unique_address]] affects the ABI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mainly that depending on the language version the ABI will be different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't be depending on the language version. [[no_unique_address]] always behaves the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the __config _LIBCPP_NO_UNIQUE_ADDRESS can have different values. Since the attribute is C++20 this value can depend on the language version used, right? What am I missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can have in theory, but it doesn't. We can simplify that to

#if __has_cpp_attribute(msvc::no_unique_address)
#  define _LIBCPP_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
#else
#  define _LIBCPP_NO_UNIQUE_ADDRESS [[no_unique_address]]
#endif

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A great, do you want to create a small PR for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change it after the release branch.

@philnik777
Copy link
Contributor Author

@huixie90 I don't think explicitly adding padding bytes is a problem here. The affected classes are in just about no way trivial, so I don't think it matters how the padding bytes are generated. @cjdb it would be great to get some data whether we could drop the padding bytes, but my suspicion is that we can't, since [[no_unique_address]] is way more powerful than EBO.

@ldionne ldionne self-assigned this Jan 18, 2024
Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't fully looked at the patch, however so far I think this is absolutely amazing. This is a great simplification and I think the only case that breaks (unless something was overlooked) is acceptable. I'll need to have another look at this.

libcxx/include/__memory/compressed_pair.h Outdated Show resolved Hide resolved
libcxx/include/__memory/compressed_pair.h Show resolved Hide resolved
libcxx/include/__memory/shared_ptr.h Outdated Show resolved Hide resolved
libcxx/include/__memory/shared_ptr.h Outdated Show resolved Hide resolved
libcxx/include/__config Outdated Show resolved Hide resolved
@ldionne ldionne added this to the LLVM 19.0.X Release milestone Jan 18, 2024
@philnik777 philnik777 force-pushed the compressed_pair_no_unique_address branch from 8d3053c to 20c2a36 Compare January 22, 2024 13:57
Copy link
Contributor

@hawkinsw hawkinsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate all your work on this and hope that these little nit fixes are helpful!

libcxx/docs/ReleaseNotes/19.rst Outdated Show resolved Hide resolved
libcxx/docs/ReleaseNotes/19.rst Outdated Show resolved Hide resolved
libcxx/docs/ReleaseNotes/19.rst Outdated Show resolved Hide resolved
Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This almost LGTM. I'd like to see it one last time before this ships since I have a few questions and this patch is quite tricky. But I think we can land it within the next few days.

libcxx/include/__config Outdated Show resolved Hide resolved
libcxx/include/__memory/compressed_pair.h Outdated Show resolved Hide resolved
libcxx/include/__memory/compressed_pair.h Outdated Show resolved Hide resolved
return __elem;
}
_LIBCPP_HIDE_FROM_ABI _Alloc* __get_alloc() _NOEXCEPT { return std::addressof(__alloc_); }
_LIBCPP_HIDE_FROM_ABI _LIBCPP_NO_CFI _Tp* __get_elem() _NOEXCEPT { return std::addressof(__elem_); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure you can (and should) drop _LIBCPP_NO_CFI here since this was only useful when we did type punning. It's fine to do in a separate PR but let's drop it.

libcxx/include/__memory/shared_ptr.h Show resolved Hide resolved
@@ -725,8 +726,7 @@ public:
private:
pointer __begin_ = nullptr;
pointer __end_ = nullptr;
__compressed_pair<pointer, allocator_type> __end_cap_ =
__compressed_pair<pointer, allocator_type>(nullptr, __default_init_tag());
_LIBCPP_COMPRESSED_PAIR(pointer, __cap_, allocator_type, __alloc_);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to restore the = nullptr here and re-simplify the ctors after my suggestion to the macro.

libcxx/docs/ReleaseNotes/19.rst Outdated Show resolved Hide resolved
@philnik777 philnik777 force-pushed the compressed_pair_no_unique_address branch from 20c2a36 to 2f30123 Compare January 31, 2024 20:28
@AdvenamTacet AdvenamTacet self-requested a review February 8, 2024 15:20
@ldionne
Copy link
Member

ldionne commented Feb 8, 2024

I just discussed with @philnik777 and I realized that this requires support for [[no_unique_address]] in C++03 mode. This seems to have landed in Clang 17, however there is no currently released AppleClang version that supports the feature, so this patch will have to sit around for a bit until we drop support for AppleClang 15. This is unfortunate, but it means this patch will likely land in the LLVM 20 time frame instead of LLVM 19, as we want to land it early in a release cycle.

@philnik777 philnik777 removed this from the LLVM 19.X Release milestone Feb 8, 2024
@philnik777 philnik777 marked this pull request as draft February 10, 2024 17:39
@philnik777 philnik777 force-pushed the compressed_pair_no_unique_address branch from 2f30123 to 4807741 Compare May 14, 2024 13:12
@philnik777 philnik777 force-pushed the compressed_pair_no_unique_address branch 3 times, most recently from 5d0b99d to da991f8 Compare June 24, 2024 18:02
Michael137 added a commit to Michael137/llvm-project that referenced this pull request Jun 24, 2024
This patch is in preparation for the `__compressed_pair`
refactor in llvm#76756.

This gets the formatter tests to at least pass.

Currently in draft because there's still some cleanup to be done.
@mstorsjo
Copy link
Member

@mstorsjo I've uploaded #108956 in case you missed it. That works in all versions and adds tests.

Ah, nice! Running a larger build case with this form included now...

@alexfh
Copy link
Contributor

alexfh commented Sep 25, 2024

I found an interesting interaction between this and the DoNotOptimize from the Google Benchmark library: https://gcc.godbolt.org/z/8eWo5hzdo

In short, DoNotOptimize stopped compiling for argument of type std::unique_ptr with a custom deleter.

error: invalid lvalue in asm output
    6 |   asm volatile("" : "+r,m"(var) : : "memory");
      |                            ^~~
<source>:24:3: note: in instantiation of function template specialization 'DoNotOptimize<std::unique_ptr<int, Delete<int, std::allocator<int>>>>' requested here
   24 |   DoNotOptimize(ptr);
      |   ^

That seems to be quite a rare case though.

Upd: We worked around the issue locally. It's a problem with the lack of appropriate means to implement DoNotSubmit() for different argument types in a convenient and non-surprising way.

@alexfh
Copy link
Contributor

alexfh commented Sep 30, 2024

Now we found a number of other problems related to debuggability of the code under LLDB. Specifically, std::vector<> and std::string contents can't be inspected. @labath and @slackito have been looking at the LLDB side of this and have some hypotheses. They have more context and can explain the issue better (and likely provide a reproducer), but overall it looks like a non-trivial problem with no obvious solution yet. Current ideas revolve around debug information quality and its consumption by LLDB.

I wonder if debuggability of code using libc++ is considered an important trait of libc++, and this issue may be viewed as a reason for a revert (or to guard this with a feature macro)? For us it's definitely the case, but I see how priorities may be different for libc++ maintainers. Unfortunately, local revert of this commit proved to be problematic due to a number of later libc++ changes in the same files.

@Michael137
Copy link
Member

Michael137 commented Sep 30, 2024

Now we found a number of other problems related to debuggability of the code under LLDB. Specifically, std::vector<> and std::string contents can't be inspected. @labath and @slackito have been looking at the LLDB side of this and have some hypotheses. They have more context and can explain the issue better (and likely provide a reproducer), but overall it looks like a non-trivial problem with no obvious solution yet. Current ideas revolve around debug information quality and its consumption by LLDB.

I wonder if debuggability of code using libc++ is considered an important trait of libc++, and this issue may be viewed as a reason for a revert (or to guard this with a feature macro)? For us it's definitely the case, but I see how priorities may be different for libc++ maintainers. Unfortunately, local revert of this commit proved to be problematic due to a number of later libc++ changes in the same files.

LLDB was pinged about this a few months ago when this effort began. And we did the work to make sure the affected libc++ types are debuggable with this change. The LLDB test-suite runs cleanly (including all the libc++ specific tests). I'd say we did as much as we could to reduce fallout for debuggability. More insight into what exactly is breaking for you would be great. Is this -flimit-debug-info/-gsimple-template-names related perhaps? Is it possible we don't have enough public buildbot coverage for those configurations? Without reproducers or CI it's going to be hard to assist you here.

@ldionne
Copy link
Member

ldionne commented Sep 30, 2024

I wonder if debuggability of code using libc++ is considered an important trait of libc++ [...]

Yes, we care about debuggability. However, I think debuggability should be better after this change since we don't use __compressed_pair anymore, which was introducing a really confusing layer of indirection. As Michael said above, I think we will need to understand exactly what you're running into before we can really form an opinion.

@labath
Copy link
Collaborator

labath commented Sep 30, 2024

I am looking into this on our end. While I don't have an exact reproducer (yet), I can provide some details. Basically I think we're dealing with two problems:

  1. lldb thinks that alignof(std::string) is one -- in the unstable ABI at least. This is the bit that is changed by this patch, and it causes issues when computing layouts of structs (containing strings). I don't know why that happens, though I seem to remember some mentions of "packed" structs on the no_unique_address patches. AFAICT, this happens for any compiler flag combination.
  2. lldb ignores DW_AT_member_offset for (some?) structs. Normally going by unnoticed, this issue significantly increases the blast radius of the previous bug. This part is triggered by -flimit-debug-info, and it occurs when lldb parses the type (the one which has std::string as a member) from a non-defining declaration (this probably also means that our changes to delay definition searching are implicated). From what I can tell, this happens because lldb causes a type to be laid out before it computes its (DWARF-driven) layout. This happens due to this line here, which triggers a recursive attempt to complete the type. Indeed, deleting that line makes the class layout come out right (despite the first bug), but:
  • I don't understand the full impact of doing that, or how this could have ever worked
  • it breaks one other test (Shell/SymbolFile/DWARF/x86/DW_AT_declaration-with-children.s) -- probably by exposing some other bug. I haven't looked into that yet, though I think/hope this should be fairly easy to fix.

@Michael137
Copy link
Member

Michael137 commented Sep 30, 2024

I am looking into this on our end. While I don't have an exact reproducer (yet), I can provide some details. Basically I think we're dealing with two problems:

  1. lldb thinks that alignof(std::string) is one -- in the unstable ABI at least. This is the bit that is changed by this patch, and it causes issues when computing layouts of structs (containing strings). I don't know why that happens, though I seem to remember some mentions of "packed" structs on the no_unique_address patches. AFAICT, this happens for any compiler flag combination.
  2. lldb ignores DW_AT_member_offset for (some?) structs. Normally going by unnoticed, this issue significantly increases the blast radius of the previous bug. This part is triggered by -flimit-debug-info, and it occurs when lldb parses the type (the one which has std::string as a member) from a non-defining declaration (this probably also means that our changes to delay definition searching are implicated). From what I can tell, this happens because lldb causes a type to be laid out before it computes its (DWARF-driven) layout. This happens due to this line here, which triggers a recursive attempt to complete the type. Indeed, deleting that line makes the class layout come out right (despite the first bug), but:
  • I don't understand the full impact of doing that, or how this could have ever worked
  • it breaks one other test (Shell/SymbolFile/DWARF/x86/DW_AT_declaration-with-children.s) -- probably by exposing some other bug. I haven't looked into that yet, though I think/hope this should be fairly easy to fix.

Thanks for the details! Regarding (1), i think you might be running into: #97443. Which was causing issues for the std::map/std::unordered_map formatters (which used to synthesize new Clang types and incorrectly calculate the FieldOffsets into those types) until that was "fixed" in #97579. Is this happening for some of your internal formatters perhaps?

@labath
Copy link
Collaborator

labath commented Sep 30, 2024

No, I'm pretty sure that's not it. This is really just the vanilla (well, unstable abi vanilla) std::string from libc++.. which somehow ends up with alignment of one.

@Michael137
Copy link
Member

No, I'm pretty sure that's not it. This is really just the vanilla (well, unstable abi vanilla) std::string from libc++.. which somehow ends up with alignment of one.

I was just wondering where/how the alignment miscalculation is manifesting. Just based on this XFAILed test: #96932, it sounds like #97443 is indeed the issue. Could very well be a red herring though!

@labath
Copy link
Collaborator

labath commented Sep 30, 2024

Could be. I think that's the issue I am remembering. FWIW, the reproducer for this part is quite simple:

$ cat /tmp/a.cc
#include <string>

std::string s;

int main() {
  return alignof(s);
}
$ bin/clang++ /tmp/a.cc -o /tmp/a.o -c -g -fstandalone-debug -stdlib=libc++ # Make sure libc++ uses the unstable ABI
/tmp/a.cc:6:10: warning: 'alignof' applied to an expression is a GNU extension [-Wgnu-alignof-expression]
    6 |   return alignof(s);
      |          ^
1 warning generated.
$ lldb /tmp/a.o -x -o 'script lldb.target.FindFirstType("::std::__2::string").GetByteAlign()'
(lldb) target create "/tmp/a.o"
Current executable set to '/tmp/a.o' (x86_64).
(lldb) script lldb.target.FindFirstType("::std::__2::string").GetByteAlign()
1

@labath
Copy link
Collaborator

labath commented Oct 1, 2024

#110648 is my attempt to fix the second problem. I don't yet know what's the deal with the first one.

@labath
Copy link
Collaborator

labath commented Oct 1, 2024

A standalone reproducer for the first part is:

struct empty {};

struct S {
  int i;
  [[no_unique_address]] empty e;
};

S s;
static_assert(alignof(S) == sizeof(int), "");

@labath
Copy link
Collaborator

labath commented Oct 1, 2024

.. which is basically the same as the test case you linked to. And this is due to the "strange layout implies packed" check, which was discussed on the PR.

  if (InferAlignment && ExternalFieldOffset < ComputedOffset) {
    // The externally-supplied field offset is before the field offset we
    // computed. Assume that the structure is packed.
    Alignment = CharUnits::One();
    PreferredAlignment = CharUnits::One();
    InferAlignment = false;
  }

What was the outcome of that discussion?

@Michael137
Copy link
Member

Michael137 commented Oct 1, 2024

.. which is basically the same as the test case you linked to. And this is due to the "strange layout implies packed" check, which was discussed on the PR.

  if (InferAlignment && ExternalFieldOffset < ComputedOffset) {
    // The externally-supplied field offset is before the field offset we
    // computed. Assume that the structure is packed.
    Alignment = CharUnits::One();
    PreferredAlignment = CharUnits::One();
    InferAlignment = false;
  }

What was the outcome of that discussion?

I'll have to go back over my discussion with @dwblaikie about this from couple of months ago. The resolution back then was to avoid having to rely on the miscalculated alignment when formatting types (basically side-stepping the issue). Not ideal, but worked for the libc++ formatters.

To fix the alignment calculation, i think we would need to either (1) emit DW_AT_alignment for packed structures (so we don't need the InferAlignment path anymore), or (2) adjust InferAlignment so it doesn't incorrectly assume a structure is packed (which is what I tried in #97443, but didn't complete). Happy to revive/discuss said PR.

@ldionne
Copy link
Member

ldionne commented Oct 1, 2024

Based on the above, given that these are understood LLDB issues and that this libc++ change has several other benefits and has been almost a year in the making, I don't think reverting the libc++ change is the right thing to do in this case.

ldionne pushed a commit that referenced this pull request Oct 1, 2024
…ion is not available (#109693)

This PR fixes the shared_ptr control block layout test that was recently updated in #76756.
When aligned allocation/deallocation is not available, part of the test doesn't work.
philnik777 added a commit to philnik777/llvm-project that referenced this pull request Oct 1, 2024
@labath
Copy link
Collaborator

labath commented Oct 2, 2024

To fix the alignment calculation, i think we would need to either (1) emit DW_AT_alignment for packed structures (so we don't need the InferAlignment path anymore), or (2) adjust InferAlignment so it doesn't incorrectly assume a structure is packed (which is what I tried in #97443, but didn't complete). Happy to revive/discuss said PR.

That would be great. I've been ignoring that patch so far as the record layout builder is not my wheelhouse, but I'm going to comment on it now.

VitaNuo pushed a commit to VitaNuo/llvm-project that referenced this pull request Oct 2, 2024
…ion is not available (llvm#109693)

This PR fixes the shared_ptr control block layout test that was recently updated in llvm#76756.
When aligned allocation/deallocation is not available, part of the test doesn't work.
VitaNuo pushed a commit to VitaNuo/llvm-project that referenced this pull request Oct 2, 2024
…ion is not available (llvm#109693)

This PR fixes the shared_ptr control block layout test that was recently updated in llvm#76756.
When aligned allocation/deallocation is not available, part of the test doesn't work.
philnik777 added a commit that referenced this pull request Oct 2, 2024
…8952)

A few functions are now unnecessary, since we can access the members
directly instread now.
Sterling-Augustine pushed a commit to Sterling-Augustine/llvm-project that referenced this pull request Oct 3, 2024
…ion is not available (llvm#109693)

This PR fixes the shared_ptr control block layout test that was recently updated in llvm#76756.
When aligned allocation/deallocation is not available, part of the test doesn't work.
Sterling-Augustine pushed a commit to Sterling-Augustine/llvm-project that referenced this pull request Oct 3, 2024
…lvm#108952)

A few functions are now unnecessary, since we can access the members
directly instread now.
xgupta pushed a commit to xgupta/llvm-project that referenced this pull request Oct 4, 2024
…ion is not available (llvm#109693)

This PR fixes the shared_ptr control block layout test that was recently updated in llvm#76756.
When aligned allocation/deallocation is not available, part of the test doesn't work.
xgupta pushed a commit to xgupta/llvm-project that referenced this pull request Oct 4, 2024
…lvm#108952)

A few functions are now unnecessary, since we can access the members
directly instread now.
@alexfh
Copy link
Contributor

alexfh commented Oct 4, 2024

#110648 has resolved the issue for us. Thank you @labath!

philnik777 added a commit to philnik777/llvm-project that referenced this pull request Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ABI Application Binary Interface enhancement Improving things as opposed to bug fixing, e.g. new or missing feature libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet